iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Rust

大家一起跟Rust當好朋友吧!系列 第 11

Day 11: 特徵 (Traits):定義共享行為 - Rust 的多型與介面系統

  • 分享至 

  • xImage
  •  

嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十一天!

昨天我們學習了泛型,掌握了如何寫出彈性且抽象的程式碼。今天我們要來探討一個與泛型緊密相關,且同樣重要的概念:特徵 (Traits)

如果說泛型讓我們能夠「寫出適用於多種型別的程式碼」,那麼 Traits 就是「定義這些型別應該具備什麼行為」的機制。你可以把 Traits 想像成其他語言中的介面 (Interface) 或抽象類別,但 Rust 的 Traits 比它們更加強大和靈活。

老實說,當我剛開始接觸 Traits 時,覺得它們就是介面的 Rust 版本。但隨著深入了解,我發現 Traits 的設計哲學更加優雅:它們不只是定義「某個型別能做什麼」,更是實現了「能力導向的程式設計」—你不需要關心物件的具體型別,只需要關心它具備什麼能力。

今天讓我們一起探索這個讓 Rust 程式碼既抽象又高效的強大特性!

什麼是 Traits?

概念介紹

Trait 是一種定義共享行為的方式。它告訴 Rust 編譯器某個特定型別具有哪些功能,並且這些功能可以與其他型別共享。

// 定義一個 Trait
trait Drawable {
    fn draw(&self);
}

// 為不同型別實現這個 Trait
struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("畫一個半徑為 {:.2} 的圓形", self.radius);
    }
}

impl Drawable for Rectangle {
    fn draw(&self) {
        println!("畫一個 {:.2} x {:.2} 的長方形", self.width, self.height);
    }
}

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 10.0, height: 8.0 };
    
    circle.draw();     // 畫一個半徑為 5.00 的圓形
    rectangle.draw();  // 畫一個 10.00 x 8.00 的長方形
}

Trait 的基本語法

定義 Trait

trait TraitName {
    // 方法簽章(無預設實現)
    fn required_method(&self) -> ReturnType;
    
    // 有預設實現的方法
    fn default_method(&self) {
        println!("這是預設實現");
    }
    
    // 關聯函式(類似靜態方法)
    fn associated_function() -> String {
        String::from("關聯函式")
    }
    
    // 關聯型別
    type AssociatedType;
    
    // 關聯常數
    const ASSOCIATED_CONST: u32 = 42;
}

實現 Trait

struct MyStruct {
    value: i32,
}

impl TraitName for MyStruct {
    type AssociatedType = String;
    
    fn required_method(&self) -> String {
        format!("值是 {}", self.value)
    }
    
    // 可以選擇覆寫預設實現
    fn default_method(&self) {
        println!("自訂的實現,值是 {}", self.value);
    }
}

實用的 Trait 範例

1. 顯示 Trait (Display)

use std::fmt;

struct Point {
    x: f64,
    y: f64,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({:.2}, {:.2})", self.x, self.y)
    }
}

struct Person {
    name: String,
    age: u32,
}

impl fmt::Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({}歲)", self.name, self.age)
    }
}

fn main() {
    let point = Point { x: 3.14, y: 2.71 };
    let person = Person { name: "Alice".to_string(), age: 25 };
    
    println!("座標點:{}", point);
    println!("人員:{}", person);
    
    // Display trait 讓我們可以使用 {} 格式化
    let formatted = format!("座標:{},人員:{}", point, person);
    println!("{}", formatted);
}

2. 比較 Trait (PartialEq, Eq, PartialOrd, Ord)

#[derive(Debug)]
struct Student {
    name: String,
    grade: u32,
    age: u32,
}

impl PartialEq for Student {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name && self.age == other.age
    }
}

impl Eq for Student {}

impl PartialOrd for Student {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Student {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        // 先按成績排序,再按年齡排序
        match self.grade.cmp(&other.grade) {
            std::cmp::Ordering::Equal => self.age.cmp(&other.age),
            other => other,
        }
    }
}

fn main() {
    let mut students = vec![
        Student { name: "Alice".to_string(), grade: 85, age: 20 },
        Student { name: "Bob".to_string(), grade: 92, age: 19 },
        Student { name: "Charlie".to_string(), grade: 85, age: 21 },
    ];
    
    // 現在可以比較學生
    println!("Alice == Bob: {}", students[0] == students[1]);
    println!("Alice < Bob: {}", students[0] < students[1]);
    
    // 現在可以排序
    students.sort();
    println!("排序後的學生:");
    for student in &students {
        println!("  {:?}", student);
    }
}

3. 克隆 Trait (Clone)

#[derive(Debug)]
struct Book {
    title: String,
    author: String,
    pages: u32,
}

impl Clone for Book {
    fn clone(&self) -> Self {
        println!("正在複製書籍:{}", self.title);
        Book {
            title: self.title.clone(),
            author: self.author.clone(),
            pages: self.pages,
        }
    }
}

fn main() {
    let original = Book {
        title: "Rust 程式設計".to_string(),
        author: "Rust 社群".to_string(),
        pages: 500,
    };
    
    let copy = original.clone();
    
    println!("原書:{:?}", original);
    println!("副本:{:?}", copy);
    
    // 證明它們是不同的實例
    println!("是同一本書嗎?{}", std::ptr::eq(&original, &copy));
}

進階 Trait 概念

1. Trait 作為參數

trait Summary {
    fn summarize(&self) -> String;
}

struct NewsArticle {
    headline: String,
    content: String,
    author: String,
}

struct Tweet {
    username: String,
    content: String,
    reply: bool,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{} - by {}", self.headline, self.author)
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("@{}: {}", self.username, self.content)
    }
}

// 接受任何實現了 Summary 的型別
fn notify(item: &impl Summary) {
    println!("突發新聞!{}", item.summarize());
}

// 等價的 Trait bound 語法
fn notify_v2<T: Summary>(item: &T) {
    println!("突發新聞!{}", item.summarize());
}

// 多個 Trait bounds
fn notify_and_display<T: Summary + std::fmt::Display>(item: &T) {
    println!("顯示:{}", item);
    println!("摘要:{}", item.summarize());
}

fn main() {
    let article = NewsArticle {
        headline: "Rust 1.75 發布!".to_string(),
        content: "新版本帶來了許多改進...".to_string(),
        author: "Rust 團隊".to_string(),
    };
    
    let tweet = Tweet {
        username: "rustlang".to_string(),
        content: "新版本發布了!快來試試看新功能 #rust".to_string(),
        reply: false,
    };
    
    notify(&article);
    notify(&tweet);
    notify_v2(&article);
}

2. 回傳實現 Trait 的型別

fn create_summary(use_tweet: bool) -> impl Summary {
    if use_tweet {
        Tweet {
            username: "example".to_string(),
            content: "這是一個範例推文".to_string(),
            reply: false,
        }
    } else {
        // 注意:這會編譯錯誤!impl Trait 只能回傳單一具體型別
        // NewsArticle { ... }
        Tweet {
            username: "news".to_string(),
            content: "新聞內容".to_string(),
            reply: false,
        }
    }
}

3. 條件實現 (Conditional Implementation)

use std::fmt::Display;

struct Pair<T> {
    first: T,
    second: T,
}

impl<T> Pair<T> {
    fn new(first: T, second: T) -> Self {
        Self { first, second }
    }
}

// 只為實現了 Display + PartialOrd 的型別實現這個方法
impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.first >= self.second {
            println!("最大的是 first = {}", self.first);
        } else {
            println!("最大的是 second = {}", self.second);
        }
    }
}

fn main() {
    let pair = Pair::new(10, 20);
    pair.cmp_display(); // 這個方法只有在 T 實現了 Display + PartialOrd 時才可用
    
    let string_pair = Pair::new("hello", "world");
    string_pair.cmp_display();
}

標準函式庫中常見的 Traits

1. Iterator Trait

struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Counter {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let current = self.current;
            self.current += 1;
            Some(current)
        } else {
            None
        }
    }
}

fn main() {
    let counter = Counter::new(5);
    
    // 現在可以使用所有迭代器方法
    let doubled: Vec<usize> = counter
        .map(|x| x * 2)
        .filter(|&x| x > 2)
        .collect();
    
    println!("雙倍過濾後:{:?}", doubled);
    
    // 使用 for 迴圈
    for num in Counter::new(3) {
        println!("計數:{}", num);
    }
}

2. From 和 Into Traits

#[derive(Debug)]
struct Person {
    name: String,
    age: u32,
}

// 實現 From trait
impl From<(&str, u32)> for Person {
    fn from(data: (&str, u32)) -> Self {
        Person {
            name: data.0.to_string(),
            age: data.1,
        }
    }
}

impl From<String> for Person {
    fn from(name: String) -> Self {
        Person { name, age: 0 }
    }
}

// Into trait 會自動實現
fn main() {
    // 使用 From
    let person1 = Person::from(("Alice", 25));
    let person2 = Person::from("Bob".to_string());
    
    println!("Person 1: {:?}", person1);
    println!("Person 2: {:?}", person2);
    
    // 使用 Into
    let person3: Person = ("Charlie", 30).into();
    let person4: Person = "Diana".to_string().into();
    
    println!("Person 3: {:?}", person3);
    println!("Person 4: {:?}", person4);
    
    // 在函式參數中使用
    create_person(("Eve", 28));
    create_person("Frank".to_string());
}

fn create_person<T: Into<Person>>(data: T) {
    let person = data.into();
    println!("建立了人員:{:?}", person);
}

3. Default Trait

#[derive(Debug)]
struct Config {
    host: String,
    port: u16,
    debug: bool,
    max_connections: u32,
}

impl Default for Config {
    fn default() -> Self {
        Config {
            host: "localhost".to_string(),
            port: 8080,
            debug: false,
            max_connections: 100,
        }
    }
}

impl Config {
    fn new() -> Self {
        Self::default()
    }
    
    fn with_host(mut self, host: String) -> Self {
        self.host = host;
        self
    }
    
    fn with_port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
    
    fn with_debug(mut self, debug: bool) -> Self {
        self.debug = debug;
        self
    }
}

fn main() {
    // 使用預設值
    let default_config = Config::default();
    println!("預設設定:{:?}", default_config);
    
    // 建構者模式
    let custom_config = Config::new()
        .with_host("example.com".to_string())
        .with_port(3000)
        .with_debug(true);
    
    println!("自訂設定:{:?}", custom_config);
}

關聯型別 (Associated Types)

關聯型別讓 Trait 更加靈活,當一個 Trait 對於一個型別只有一種合理的實現時使用。

trait Container {
    type Item;
    
    fn get(&self, index: usize) -> Option<&Self::Item>;
    fn len(&self) -> usize;
    fn push(&mut self, item: Self::Item);
}

struct IntContainer {
    items: Vec<i32>,
}

impl Container for IntContainer {
    type Item = i32;
    
    fn get(&self, index: usize) -> Option<&Self::Item> {
        self.items.get(index)
    }
    
    fn len(&self) -> usize {
        self.items.len()
    }
    
    fn push(&mut self, item: Self::Item) {
        self.items.push(item);
    }
}

struct StringContainer {
    items: Vec<String>,
}

impl Container for StringContainer {
    type Item = String;
    
    fn get(&self, index: usize) -> Option<&Self::Item> {
        self.items.get(index)
    }
    
    fn len(&self) -> usize {
        self.items.len()
    }
    
    fn push(&mut self, item: Self::Item) {
        self.items.push(item);
    }
}

// 泛型函式使用關聯型別
fn print_container<C: Container>(container: &C) 
where
    C::Item: std::fmt::Display,
{
    println!("容器包含 {} 個項目:", container.len());
    for i in 0..container.len() {
        if let Some(item) = container.get(i) {
            println!("  [{}]: {}", i, item);
        }
    }
}

fn main() {
    let mut int_container = IntContainer { items: vec![] };
    int_container.push(1);
    int_container.push(2);
    int_container.push(3);
    
    let mut string_container = StringContainer { items: vec![] };
    string_container.push("Hello".to_string());
    string_container.push("World".to_string());
    
    print_container(&int_container);
    print_container(&string_container);
}

Trait Objects:動態分配

有時候我們需要在執行時決定要呼叫哪個實現,這時可以使用 Trait Objects。

trait Animal {
    fn make_sound(&self);
    fn name(&self) -> &str;
}

struct Dog {
    name: String,
}

struct Cat {
    name: String,
}

struct Bird {
    name: String,
}

impl Animal for Dog {
    fn make_sound(&self) {
        println!("汪汪!");
    }
    
    fn name(&self) -> &str {
        &self.name
    }
}

impl Animal for Cat {
    fn make_sound(&self) {
        println!("喵喵!");
    }
    
    fn name(&self) -> &str {
        &self.name
    }
}

impl Animal for Bird {
    fn make_sound(&self) {
        println!("啾啾!");
    }
    
    fn name(&self) -> &str {
        &self.name
    }
}

fn main() {
    // 使用 Trait Objects 儲存不同型別的動物
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog { name: "小白".to_string() }),
        Box::new(Cat { name: "小花".to_string() }),
        Box::new(Bird { name: "小鳥".to_string() }),
    ];
    
    // 動態分配:在執行時決定呼叫哪個實現
    for animal in &animals {
        println!("{} 說:", animal.name());
        animal.make_sound();
    }
    
    // 也可以使用參考
    let dog = Dog { name: "大黃".to_string() };
    let cat = Cat { name: "小黑".to_string() };
    
    let animal_refs: Vec<&dyn Animal> = vec![&dog, &cat];
    
    println!("\n使用參考:");
    for animal in animal_refs {
        println!("{} 說:", animal.name());
        animal.make_sound();
    }
}

進階 Trait 技巧

1. 父 Trait (Supertraits)

trait Animal {
    fn name(&self) -> &str;
}

trait Dog: Animal {  // Dog 是 Animal 的子 trait
    fn bark(&self);
}

struct Labrador {
    name: String,
}

impl Animal for Labrador {
    fn name(&self) -> &str {
        &self.name
    }
}

impl Dog for Labrador {
    fn bark(&self) {
        println!("{} 說:汪汪!", self.name());
    }
}

fn main() {
    let dog = Labrador { name: "小白".to_string() };
    dog.bark(); // 可以使用 Dog trait 的方法
    println!("狗的名字是:{}", dog.name()); // 也可以使用 Animal trait 的方法
}

2. 運算子重載

use std::ops::{Add, Mul, Display};
use std::fmt;

#[derive(Debug, Clone, Copy)]
struct Vector2D {
    x: f64,
    y: f64,
}

impl Vector2D {
    fn new(x: f64, y: f64) -> Self {
        Vector2D { x, y }
    }
    
    fn magnitude(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

impl Add for Vector2D {
    type Output = Vector2D;
    
    fn add(self, other: Vector2D) -> Vector2D {
        Vector2D {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

impl Mul<f64> for Vector2D {
    type Output = Vector2D;
    
    fn mul(self, scalar: f64) -> Vector2D {
        Vector2D {
            x: self.x * scalar,
            y: self.y * scalar,
        }
    }
}

impl fmt::Display for Vector2D {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({:.2}, {:.2})", self.x, self.y)
    }
}

fn main() {
    let v1 = Vector2D::new(1.0, 2.0);
    let v2 = Vector2D::new(3.0, 4.0);
    
    let v3 = v1 + v2;  // 使用 Add trait
    let v4 = v1 * 2.0; // 使用 Mul trait
    
    println!("v1: {}", v1);
    println!("v2: {}", v2);
    println!("v1 + v2: {}", v3);
    println!("v1 * 2: {}", v4);
    println!("v3 的長度: {:.2}", v3.magnitude());
}

Trait 的最佳實務

1. 小而專一的 Traits

// ✅ 好:小而專一的 traits
trait Readable {
    fn read(&self) -> String;
}

trait Writable {
    fn write(&mut self, data: &str) -> Result<(), String>;
}

trait Seekable {
    fn seek(&mut self, position: u64) -> Result<(), String>;
}

// 需要多種能力時組合使用
fn process_file<T>(file: &mut T) -> Result<String, String>
where
    T: Readable + Writable + Seekable,
{
    file.seek(0)?;
    let content = file.read();
    file.write(&format!("處理過的:{}", content))?;
    Ok(content)
}

// ❌ 不好:過大的 trait
trait FileTrait {
    fn read(&self) -> String;
    fn write(&mut self, data: &str) -> Result<(), String>;
    fn seek(&mut self, position: u64) -> Result<(), String>;
    fn compress(&self) -> Vec<u8>;
    fn encrypt(&self, key: &str) -> Vec<u8>;
    // ... 太多職責
}

2. 有意義的命名

// ✅ 好的命名
trait Drawable {
    fn draw(&self);
}

trait Serializable {
    fn serialize(&self) -> String;
}

trait Comparable {
    fn compare(&self, other: &Self) -> std::cmp::Ordering;
}

// ❌ 不好的命名
trait Thing {  // 太模糊
    fn do_stuff(&self);
}

trait T {  // 沒有意義
    fn method(&self);
}

3. 適當使用預設實現

trait Logger {
    fn log(&self, message: &str);
    
    // 提供預設實現
    fn info(&self, message: &str) {
        self.log(&format!("[INFO] {}", message));
    }
    
    fn error(&self, message: &str) {
        self.log(&format!("[ERROR] {}", message));
    }
    
    fn debug(&self, message: &str) {
        self.log(&format!("[DEBUG] {}", message));
    }
}

struct ConsoleLogger;

impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("{}", message);
    }
    
    // 可以選擇性覆寫預設實現
    fn error(&self, message: &str) {
        eprintln!("❌ {}", message);
    }
}

fn main() {
    let logger = ConsoleLogger;
    logger.info("應用程式啟動");
    logger.error("發生錯誤");
    logger.debug("除錯資訊");
}

Traits vs 其他語言的比較

與 Java/C# 介面的差異

// Rust Traits 支援預設實現
trait Shape {
    fn area(&self) -> f64;
    
    // 預設實現
    fn describe(&self) -> String {
        format!("這是一個面積為 {:.2} 的形狀", self.area())
    }
}

// 可以為現有型別實現 traits(擴展性)
impl Shape for f64 {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self * self  // 假設這是圓的半徑
    }
}

fn main() {
    let radius = 5.0;
    println!("{}", radius.describe()); // f64 現在有了 describe 方法!
}

與 Go 介面的差異

// Rust 的 traits 是顯式實現的,不像 Go 的隱式實現
trait Writer {
    fn write(&mut self, data: &[u8]) -> Result<usize, String>;
}

struct FileWriter {
    filename: String,
}

// 必須明確聲明實現 Writer trait
impl Writer for FileWriter {
    fn write(&mut self, data: &[u8]) -> Result<usize, String> {
        println!("寫入 {} 個位元組到 {}", data.len(), self.filename);
        Ok(data.len())
    }
}

常見陷阱與除錯技巧

1. Trait 實現衝突

trait A {
    fn method(&self);
}

trait B {
    fn method(&self);
}

struct MyStruct;

impl A for MyStruct {
    fn method(&self) {
        println!("來自 trait A");
    }
}

impl B for MyStruct {
    fn method(&self) {
        println!("來自 trait B");
    }
}

fn main() {
    let obj = MyStruct;
    
    // obj.method();  // 編譯錯誤:模糊的方法呼叫
    
    // 明確指定要呼叫哪個 trait 的方法
    A::method(&obj);
    B::method(&obj);
    
    // 或使用 UFCS (Universal Function Call Syntax)
    <MyStruct as A>::method(&obj);
    <MyStruct as B>::method(&obj);
}

今天的收穫

今天我們深入學習了 Rust 的 Traits 系統:

核心概念

  • Trait 定義:使用 trait 關鍵字定義共享行為
  • Trait 實現:使用 impl Trait for Type 語法
  • 預設實現:在 trait 中提供方法的預設行為
  • 關聯型別:使用 type 關鍵字定義關聯型別

進階特性

  • Trait Bounds:約束泛型參數必須實現特定 trait
  • Trait Objects:使用 dyn Trait 進行動態分配
  • 父 Trait:定義 trait 之間的繼承關係
  • 運算子重載:實現 AddMul 等運算 trait

實用技巧

  • 條件實現:只為滿足特定條件的型別實現方法
  • 擴展性:為現有型別添加新功能
  • 組合性:小而專一的 trait 可以組合使用
  • 型別安全:在編譯時保證行為的正確性

設計原則

  • 小而專一的 trait 設計
  • 有意義的命名慣例
  • 適當使用預設實現
  • 避免 trait 實現衝突

為什麼 Traits 很重要?

  • 抽象化:定義行為而不關心具體實現
  • 程式碼重用:一套邏輯適用於多種型別
  • 擴展性:可以為現有型別添加新功能
  • 組合優於繼承:靈活的能力組合機制

Traits 是 Rust 實現多型和程式碼抽象的核心機制。它們不僅讓我們能夠寫出更加靈活和可重用的程式碼,還保持了 Rust 的零成本抽象原則。掌握 Traits 將大大提升你設計優雅 Rust API 的能力。

今天的小挑戰

為了鞏固今天的學習,嘗試設計一個通用的資料處理管線系統

功能需求

  1. 處理器 Trait:定義資料處理的基本介面
  2. 多種處理器:實現過濾、轉換、聚合等不同的處理器
  3. 管線組合:能夠將多個處理器串聯起來
  4. 錯誤處理:優雅地處理處理過程中的錯誤
  5. 統計功能:追蹤每個處理器的效能指標

技術要求

  • 使用 trait 定義處理器介面
  • 支援泛型資料型別
  • 實現 trait bounds 確保型別安全
  • 使用關聯型別處理錯誤
  • 提供預設實現以簡化使用

技術提示

trait Processor {
    type Input;
    type Output;
    type Error;
    
    fn process(&mut self, input: Self::Input) -> Result<Self::Output, Self::Error>;
    
    // 預設實現
    fn name(&self) -> &'static str {
        "未命名處理器"
    }
}

trait Pipeline<T> {
    type Error;
    
    fn add_processor<P>(self, processor: P) -> Self
    where
        P: Processor<Input = T, Output = T>;
    
    fn execute(&mut self, input: T) -> Result<T, Self::Error>;
}

// 實現各種處理器:FilterProcessor, MapProcessor, ReduceProcessor 等

這個挑戰將讓你綜合運用 traits 的各種特性:trait 定義、關聯型別、泛型約束、預設實現等。重點是設計一個既靈活又易用的處理管線系統。

明天我們將學習 生命週期 (Lifetimes),這是 Rust 中最具挑戰性但也最重要的概念之一。生命週期與 traits 和泛型結合使用,能讓我們寫出既安全又高效的程式碼!

如果在實作過程中遇到任何問題,歡迎在留言區討論。Traits 是 Rust 程式設計的精髓,多練習不同的設計模式會讓你對 Rust 的抽象能力有更深的理解!

我們明天見!


上一篇
Day 10: 泛型 (Generics):寫出彈性又抽象的程式碼
下一篇
Day 12: 生命週期 (Lifetimes):攻克 Rust 最難懂的概念
系列文
大家一起跟Rust當好朋友吧!19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言